package file_watcher

import (
	"errors"
	"golang.org/x/sys/unix"
	"log"
	"os"
	"strings"
	"sync"
	"unsafe"
)

const ListenEvents = unix.IN_MODIFY | unix.IN_DELETE_SELF

type FileInfo struct {
	Path string
	Name string
	Nwfd int
	Offset int
}

type Event struct {
	Info *FileInfo
	Option  string
}

type FileWatcher struct {

	RecvEvents chan Event

	ep *epller

	watcherFd int

	files map[string]*FileInfo

	fileMapFd map[int]string

	lock sync.Mutex
}

const Filewatcher  = "Filewatcher"

func NewFileWatcher()(*FileWatcher,error){

	var err error

	Fd, err := unix.InotifyInit1(unix.IN_CLOEXEC)

	if err != nil{
	   return nil,err
	}

	ep,err := Newepller()
	if err != nil{
		return nil,err
	}

	err = ep.AddFd(Filewatcher,Fd)
	if err != nil{
		return nil,err
	}
	var lock sync.Mutex

	return &FileWatcher{make(chan Event),ep,Fd,make(map[string]*FileInfo),
		make(map[int]string),lock},nil
}

func (fw *FileWatcher)Del(name string)(err error) {

    var watchdesc int
	for desc,fname := range fw.fileMapFd{
		if fname == name{
			watchdesc = desc
			break
		}
	}
	n,err := unix.InotifyRmWatch(fw.watcherFd,uint32(watchdesc))
    if n != 0{
    	return err
	}

	delete(fw.files,name)
	delete(fw.fileMapFd,watchdesc)

	return
}

func (fw *FileWatcher)Add(file *FileInfo)(error){

	var err error

	if fw.files[file.Name] != nil{
		return errors.New("the path already exist")
	}

	fw.files[file.Name] = file

    watchdesc,err := unix.InotifyAddWatch(fw.watcherFd,file.Path,uint32(ListenEvents))
    if watchdesc == -1{
    	return err
	}
    fw.fileMapFd[watchdesc] = file.Name
	return nil
}

func (fw *FileWatcher)ReadEvents(){
	var (
		buf   [unix.SizeofInotifyEvent * 4096]byte
		n     int
		err error
		deleteFlag uint32
	)

	defer fw.Close()
	defer fw.ep.CloseFd()

    for {
    	err = fw.ep.EpWait()
    	if err != nil{
           log.Fatal(err)
		}

		n, err = unix.Read(fw.watcherFd, buf[:])
		if err == unix.EINTR {
			continue
		}

		if n < unix.SizeofInotifyEvent {
			if n < 0 {
				log.Fatal(err)
			} else {
				// Read was too short.
				log.Fatal(errors.New("notify: short read in readEvents()"))
			}
		}

		var offset uint32

		for offset <= uint32(n-unix.SizeofInotifyEvent) {

			raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))

			mask := uint32(raw.Mask)
			nameLen := uint32(raw.Len)

			name := fw.fileMapFd[int(raw.Wd)]
			if nameLen > 0 {
				bytesA := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))
				name += "/" + strings.TrimRight(string(bytesA[0:nameLen]), "\000")
			}

			if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF{
			   deleteFlag = mask
			   offset += unix.SizeofInotifyEvent + nameLen
			   continue
			}

			if mask&unix.IN_IGNORED == unix.IN_IGNORED && deleteFlag&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF{
				path := fw.files[name].Path
				_,err = os.Stat(path)
				if err != nil{
					mask = 1024
				}else {
					mask = fw.IgnoredEvent(name)
				}
			}

			if mask&unix.IN_Q_OVERFLOW != 0 {
               log.Fatal("SizeofInotifyEvent overflow,the event is not deal")
			}

            option := getOption(mask)
			fw.RecvEvents <- Event{fw.files[name],option}

			//log.Fatal(mask)
			offset += unix.SizeofInotifyEvent + nameLen
		}
	}
}

func (fw *FileWatcher)IgnoredEvent(name string)(uint32){

	path := fw.files[name].Path

	watchdesc,err := unix.InotifyAddWatch(fw.watcherFd,path,uint32(ListenEvents))
	if watchdesc == -1{
		log.Fatal("IgnoredEvent " + err.Error())
	}
	fw.fileMapFd[watchdesc] = name

	return 2
}

func (fw *FileWatcher)ListenEvent(){
     for {
		 select {
     	    case ev := <- fw.RecvEvents:
				switch ev.Option {
				     case "write":
				     	   go ModifyHandler(&ev)
				     	  break
				     case "delete":

					      break
				}
		}
	 }
}

func getOption(mask uint32)(string){

	if mask&unix.IN_MODIFY == unix.IN_MODIFY{
       return "write"
	}else if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF{
	   return "delete"
	}else if mask&unix.IN_IGNORED == unix.IN_IGNORED {
		return "delete"
	}
    log.Fatal("bad mask")
	return ""
}

func (fw *FileWatcher)Close()(){
	if fw.watcherFd != -1{
		unix.Close(fw.watcherFd)
	}

	for _,file := range fw.files{
		if file.Nwfd != -1{
			unix.Close(file.Nwfd)
		}
	}
}